shortcuts: Mananage managed shortcuts with a custom model
authorBenjamin Otte <otte@redhat.com>
Mon, 20 Aug 2018 04:42:22 +0000 (06:42 +0200)
committerMatthias Clasen <mclasen@redhat.com>
Thu, 19 Mar 2020 03:00:51 +0000 (23:00 -0400)
Reduce the amount of special casing by using a list model
for global and managed shortcuts, too.

This way, the ListModel API will work for the ShortcutController in the
GtkShortcutManager and GtkRoot.

The only special case remaining is shortcut activation, which needs to
pass the right widget to the controller in the global/managed case.

gtk/gtkconcatmodel.c [new file with mode: 0644]
gtk/gtkconcatmodelprivate.h [new file with mode: 0644]
gtk/gtkshortcutcontroller.c
gtk/gtkshortcutcontrollerprivate.h
gtk/gtkshortcutmanager.c
gtk/gtkwidget.c
gtk/meson.build
testsuite/gtk/concatmodel.c [new file with mode: 0644]
testsuite/gtk/meson.build

diff --git a/gtk/gtkconcatmodel.c b/gtk/gtkconcatmodel.c
new file mode 100644 (file)
index 0000000..d96290e
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+
+/*
+ * SECTION:gtkconcatmodel
+ * @Short_description: concatenation list model
+ * @Title: GtkConcatModel
+ * @See_also: #GListModel
+ *
+ * #GtkConcatModel is a #GListModel implementation that takes a list of list models
+ * and presents them as one concatenated list.
+ *
+ * Node that all the types of the passed in list models must match the concat model's
+ * type. If they are not, you must use a common ancestor type for the #GtkConcatModel,
+ * %G_TYPE_OBJECT being the ultimate option.
+ **/
+
+#include "config.h"
+
+#include "gtkconcatmodelprivate.h"
+
+struct _GtkConcatModel
+{
+  GObject parent_instance;
+
+  GType item_type;
+  guint n_items;
+
+  GList *models;
+};
+
+struct _GtkConcatModelClass
+{
+  GObjectClass parent_class;
+};
+
+static GType
+gtk_concat_model_list_model_get_item_type (GListModel *list)
+{
+  GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+
+  return self->item_type;
+}
+
+static guint
+gtk_concat_model_list_model_get_n_items (GListModel *list)
+{
+  GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+
+  return self->n_items;
+}
+
+static gpointer
+gtk_concat_model_list_model_get_item (GListModel *list,
+                                      guint       position)
+{
+  GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+  GList *l;
+
+  /* FIXME: Use an RBTree to make this O(log N) */
+  for (l = self->models; l; l = l->next)
+    {
+      guint n = g_list_model_get_n_items (l->data);
+
+      if (position < n)
+        return g_list_model_get_item (l->data, position);
+
+      position -= n;
+    }
+
+  return NULL;
+}
+
+static void
+gtk_concat_model_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item_type = gtk_concat_model_list_model_get_item_type;
+  iface->get_n_items = gtk_concat_model_list_model_get_n_items;
+  iface->get_item = gtk_concat_model_list_model_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GtkConcatModel, gtk_concat_model,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_concat_model_list_model_init))
+
+static void
+gtk_concat_model_items_changed (GListModel     *model,
+                                guint           position,
+                                guint           removed,
+                                guint           added,
+                                GtkConcatModel *self)
+{
+  GList *l;
+
+  for (l = self->models; l; l = l->next)
+    {
+      if (l->data == model)
+        break;
+
+      position += g_list_model_get_n_items (l->data);
+    }
+
+  self->n_items -= removed;
+  self->n_items += added;
+
+  g_list_model_items_changed (G_LIST_MODEL (self),
+                              position,
+                              removed,
+                              added);
+}
+
+static void
+gtk_concat_model_remove_internal (GtkConcatModel *self,
+                                  GListModel     *model,
+                                  gboolean        emit_signals)
+{
+  guint n_items, position;
+  GList *l;
+
+  position = 0;
+  for (l = self->models; l; l = l->next)
+    {
+      if (l->data == model)
+        break;
+
+      position += g_list_model_get_n_items (l->data);
+    }
+
+  g_return_if_fail (l != NULL);
+
+  self->models = g_list_delete_link (self->models, l);
+  n_items = g_list_model_get_n_items (model);
+  self->n_items -= n_items;
+  g_signal_handlers_disconnect_by_func (model, gtk_concat_model_items_changed, self);
+  g_object_unref (model);
+  
+  if (n_items && emit_signals)
+    g_list_model_items_changed (G_LIST_MODEL (self),
+                                position,
+                                n_items,
+                                0);
+}
+
+static void
+gtk_concat_model_dispose (GObject *object)
+{
+  GtkConcatModel *self = GTK_CONCAT_MODEL (object);
+
+  /* FIXME: Make this work without signal emissions */
+  while (self->models)
+    gtk_concat_model_remove_internal (self, self->models->data, FALSE);
+
+  G_OBJECT_CLASS (gtk_concat_model_parent_class)->dispose (object);
+}
+
+static void
+gtk_concat_model_class_init (GtkConcatModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gtk_concat_model_dispose;
+}
+
+static void
+gtk_concat_model_init (GtkConcatModel *self)
+{
+}
+
+GtkConcatModel *
+gtk_concat_model_new (GType item_type)
+{
+  GtkConcatModel *self;
+
+  g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL);
+
+  self = g_object_new (GTK_TYPE_CONCAT_MODEL, NULL);
+
+  self->item_type = item_type;
+
+  return self;
+}
+
+void
+gtk_concat_model_append (GtkConcatModel *self,
+                         GListModel     *model)
+{
+  guint n_items;
+
+  g_return_if_fail (GTK_IS_CONCAT_MODEL (self));
+  g_return_if_fail (G_IS_LIST_MODEL (model));
+  g_return_if_fail (g_type_is_a (g_list_model_get_item_type (model), self->item_type));
+
+  g_object_ref (model);
+  g_signal_connect (model, "items-changed", G_CALLBACK (gtk_concat_model_items_changed), self);
+  self->models = g_list_append (self->models, model);
+  n_items = g_list_model_get_n_items (model);
+  self->n_items += n_items;
+  
+  if (n_items)
+    g_list_model_items_changed (G_LIST_MODEL (self),
+                                self->n_items - n_items,
+                                0,
+                                n_items);
+}
+
+void
+gtk_concat_model_remove (GtkConcatModel *self,
+                         GListModel     *model)
+{
+  g_return_if_fail (GTK_IS_CONCAT_MODEL (self));
+  g_return_if_fail (G_IS_LIST_MODEL (model));
+
+  gtk_concat_model_remove_internal (self, model, TRUE);
+}
+
+GListModel *
+gtk_concat_model_get_model_for_item (GtkConcatModel *self,
+                                     guint           position)
+{
+  GList *l;
+
+  g_return_val_if_fail (GTK_IS_CONCAT_MODEL (self), NULL);
+
+  /* FIXME: Use an RBTree to make this O(log N) */
+  for (l = self->models; l; l = l->next)
+    {
+      guint n = g_list_model_get_n_items (l->data);
+
+      if (position < n)
+        return l->data;
+
+      position -= n;
+    }
+
+  return NULL;
+}
+
diff --git a/gtk/gtkconcatmodelprivate.h b/gtk/gtkconcatmodelprivate.h
new file mode 100644 (file)
index 0000000..005a9f3
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+
+#ifndef __GTK_CONCAT_MODEL_H__
+#define __GTK_CONCAT_MODEL_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_CONCAT_MODEL         (gtk_concat_model_get_type ())
+#define GTK_CONCAT_MODEL(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_CONCAT_MODEL, GtkConcatModel))
+#define GTK_CONCAT_MODEL_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), GTK_TYPE_CONCAT_MODEL, GtkConcatModelClass))
+#define GTK_IS_CONCAT_MODEL(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_CONCAT_MODEL))
+#define GTK_IS_CONCAT_MODEL_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GTK_TYPE_CONCAT_MODEL))
+#define GTK_CONCAT_MODEL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GTK_TYPE_CONCAT_MODEL, GtkConcatModelClass))
+
+typedef struct _GtkConcatModel GtkConcatModel;
+typedef struct _GtkConcatModelClass GtkConcatModelClass;
+
+GType                   gtk_concat_model_get_type                (void) G_GNUC_CONST;
+
+GtkConcatModel *        gtk_concat_model_new                     (GType                  item_type);
+
+void                    gtk_concat_model_append                  (GtkConcatModel        *self,
+                                                                  GListModel            *model);
+void                    gtk_concat_model_remove                  (GtkConcatModel        *self,
+                                                                  GListModel            *model);
+
+GListModel *            gtk_concat_model_get_model_for_item      (GtkConcatModel        *self,
+                                                                  guint                  position);
+
+G_END_DECLS
+
+#endif /* __GTK_CONCAT_MODEL_H__ */
index f2afe0354c129e0f5f5b65af2ce9324e490f7a89..045e60df5aef8b46856a3746c1e97ae4f11890e7 100644 (file)
@@ -34,6 +34,7 @@
 
 #include "gtkshortcutcontrollerprivate.h"
 
+#include "gtkconcatmodelprivate.h"
 #include "gtkeventcontrollerprivate.h"
 #include "gtkintl.h"
 #include "gtkshortcut.h"
@@ -53,7 +54,6 @@ struct _GtkShortcutController
   GdkModifierType mnemonics_modifiers;
 
   guint custom_shortcuts : 1;
-  guint run_managed : 1;
 };
 
 struct _GtkShortcutControllerClass
@@ -212,15 +212,27 @@ gtk_shortcut_controller_finalize (GObject *object)
 static gboolean
 gtk_shortcut_controller_trigger_shortcut (GtkShortcutController *self,
                                           GtkShortcut           *shortcut,
+                                          guint                  position,
                                           GdkEvent              *event,
                                           gboolean               enable_mnemonics)
 {
+  GtkWidget *widget;
+
   if (!gtk_shortcut_trigger_trigger (gtk_shortcut_get_trigger (shortcut), event, enable_mnemonics))
     return FALSE;
 
+  widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (self));
+  if (!self->custom_shortcuts &&
+      GTK_IS_CONCAT_MODEL (self->shortcuts))
+    {
+      GListModel *model = gtk_concat_model_get_model_for_item (GTK_CONCAT_MODEL (self->shortcuts), position);
+      if (GTK_IS_SHORTCUT_CONTROLLER (model))
+        widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (model));
+    }
+
   return gtk_shortcut_action_activate (gtk_shortcut_get_action (shortcut),
                                        GTK_SHORTCUT_ACTION_EXCLUSIVE, /* FIXME */
-                                       gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (self)),
+                                       widget,
                                        gtk_shortcut_get_arguments (shortcut));
 }
 
@@ -232,34 +244,18 @@ gtk_shortcut_controller_run_controllers (GtkEventController *controller,
                                          gboolean            enable_mnemonics)
 {
   GtkShortcutController *self = GTK_SHORTCUT_CONTROLLER (controller);
-  GtkWidget *widget;
-  const GSList *l;
   guint i;
 
   for (i = 0; i < g_list_model_get_n_items (self->shortcuts); i++)
     {
       if (gtk_shortcut_controller_trigger_shortcut (self, 
                                                     g_list_model_get_item (self->shortcuts, i),
+                                                    i,
                                                     event,
                                                     enable_mnemonics))
         return TRUE;
     }
 
-  if (self->run_managed)
-    {
-      GtkPropagationPhase current_phase = gtk_event_controller_get_propagation_phase (controller);
-      widget = gtk_event_controller_get_widget (controller); 
-      
-      for (l = g_object_get_data (G_OBJECT (widget), "gtk-shortcut-controllers"); l; l = l->next)
-        {
-          if (gtk_event_controller_get_propagation_phase (l->data) != current_phase)
-            continue;
-
-          if (gtk_shortcut_controller_run_controllers (l->data, event, x, y, enable_mnemonics))
-            return TRUE;
-        }
-    }
-
   return FALSE;
 }
 
@@ -483,13 +479,6 @@ gtk_shortcut_controller_new_for_model (GListModel *model)
                        NULL);
 }
 
-void
-gtk_shortcut_controller_set_run_managed (GtkShortcutController  *controller,
-                                         gboolean                run_managed)
-{
-  controller->run_managed = run_managed;
-}
-
 /**
  * gtk_shortcut_controller_add_shortcut:
  * @self: the controller
index a7086cd678d189361fac4f3f7e7c624d71be4be9..db1b52cecc8ed8aa1eaea008652c0cac73bde2f6 100644 (file)
@@ -22,9 +22,6 @@
 
 #include "gtkshortcutcontroller.h"
 
-void                    gtk_shortcut_controller_set_run_managed         (GtkShortcutController  *controller,
-                                                                         gboolean                run_managed);
-
 void                    gtk_shortcut_controller_root                    (GtkShortcutController  *controller);
 void                    gtk_shortcut_controller_unroot                  (GtkShortcutController  *controller);
 
index 6853db9ce533659034a617ea10967800f5c6c127..db97448b6782a433cb24fc1f86b32b92a20a3800 100644 (file)
@@ -20,6 +20,7 @@
 #include "config.h"
 
 #include "gtkshortcutmanager.h"
+#include "gtkconcatmodelprivate.h"
 
 /**
  * SECTION:gtkshortcutmanager
  * The GtkShortcutManager interface is used to implement
  * shortcut scopes.
  */
+
 G_DEFINE_INTERFACE (GtkShortcutManager, gtk_shortcut_manager, G_TYPE_OBJECT)
 
-static void
-complain_if_reached (gpointer should_be_gone)
+static GtkConcatModel *
+gtk_shortcut_manager_get_model (GtkShortcutManager  *self,
+                                GtkPropagationPhase  phase)
 {
-  g_critical ("Shortcut controllers failed to clean up.");
+  switch (phase)
+    {
+    case GTK_PHASE_CAPTURE:
+      return g_object_get_data (G_OBJECT (self), "gtk-shortcut-manager-capture");
+    case GTK_PHASE_BUBBLE:
+      return g_object_get_data (G_OBJECT (self), "gtk-shortcut-manager-bubble");
+    case GTK_PHASE_NONE:
+    case GTK_PHASE_TARGET:
+      return NULL;
+    default:
+      g_assert_not_reached ();
+      return NULL;
+    }
 }
 
 static void
 gtk_shortcut_manager_default_add_controller (GtkShortcutManager    *self,
                                              GtkShortcutController *controller)
 {
-  GSList *controllers;
+  GtkConcatModel *model;
 
-  controllers = g_object_steal_data (G_OBJECT (self), "gtk-shortcut-controllers");
-  controllers = g_slist_prepend (controllers, g_object_ref (controller));
-  g_object_set_data_full (G_OBJECT (self), "gtk-shortcut-controllers", controllers, complain_if_reached);
+  model = gtk_shortcut_manager_get_model (self, 
+                                          gtk_event_controller_get_propagation_phase (GTK_EVENT_CONTROLLER (controller)));
+  if (model)
+    gtk_concat_model_append (model, G_LIST_MODEL (controller));
 }
 
 static void
 gtk_shortcut_manager_default_remove_controller (GtkShortcutManager    *self,
                                                 GtkShortcutController *controller)
 {
-  GSList *controllers;
+  GtkConcatModel *model;
 
-  controllers = g_object_steal_data (G_OBJECT (self), "gtk-shortcut-controllers");
-  controllers = g_slist_remove (controllers, controller);
-  if (controllers)
-    g_object_set_data_full (G_OBJECT (self), "gtk-shortcut-controllers", controllers, complain_if_reached);
-  g_object_unref (controller);
+  model = gtk_shortcut_manager_get_model (self, 
+                                          gtk_event_controller_get_propagation_phase (GTK_EVENT_CONTROLLER (controller)));
+  if (model)
+    gtk_concat_model_remove (model, G_LIST_MODEL (controller));
 }
 
 static void
index 71d31fda46423634a1072d6430d5158f3865a090..4be35728dfedfe755d93ce69310d335e890dca8d 100644 (file)
@@ -31,6 +31,7 @@
 #include "gtkapplicationprivate.h"
 #include "gtkbuildable.h"
 #include "gtkbuilderprivate.h"
+#include "gtkconcatmodelprivate.h"
 #include "gtkcontainerprivate.h"
 #include "gtkcssboxesprivate.h"
 #include "gtkcssfiltervalueprivate.h"
@@ -2462,13 +2463,17 @@ gtk_widget_init (GTypeInstance *instance, gpointer g_class)
 
   if (g_type_is_a (G_TYPE_FROM_CLASS (g_class), GTK_TYPE_SHORTCUT_MANAGER))
     {
-      controller = gtk_shortcut_controller_new ();
-      gtk_shortcut_controller_set_run_managed (GTK_SHORTCUT_CONTROLLER (controller), TRUE);
+      GtkConcatModel *model;
+      
+      model = gtk_concat_model_new (GTK_TYPE_SHORTCUT);
+      g_object_set_data_full (G_OBJECT (widget), "gtk-shortcut-manager-bubble", model, g_object_unref);
+      controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (model));
       gtk_widget_add_controller (widget, controller);
 
-      controller = gtk_shortcut_controller_new ();
+      model = gtk_concat_model_new (GTK_TYPE_SHORTCUT);
+      g_object_set_data_full (G_OBJECT (widget), "gtk-shortcut-manager-capture", model, g_object_unref);
+      controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (model));
       gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_CAPTURE);
-      gtk_shortcut_controller_set_run_managed (GTK_SHORTCUT_CONTROLLER (controller), TRUE);
       gtk_widget_add_controller (widget, controller);
     }
 
index e85dceac45d5a22c813ae76863000f7a70f2d1f1..095b71158dfe2d09ddaed270ac9f659859238ce3 100644 (file)
@@ -199,6 +199,7 @@ gtk_public_sources = files([
   'gtkcombobox.c',
   'gtkcomboboxtext.c',
   'gtkcomposetable.c',
+  'gtkconcatmodel.c',
   'gtkconstraintguide.c',
   'gtkconstraintlayout.c',
   'gtkconstraint.c',
diff --git a/testsuite/gtk/concatmodel.c b/testsuite/gtk/concatmodel.c
new file mode 100644 (file)
index 0000000..3e6292b
--- /dev/null
@@ -0,0 +1,372 @@
+/* GtkRBTree tests.
+ *
+ * Copyright (C) 2011, Red Hat, Inc.
+ * Authors: Benjamin Otte <otte@gnome.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <locale.h>
+
+#include "../../gtk/gtkconcatmodelprivate.h"
+
+/* _gtk_rbtree_test */
+
+static GQuark number_quark;
+static GQuark changes_quark;
+
+static guint
+get (GListModel *model,
+     guint       position)
+{
+  GObject *object = g_list_model_get_item (model, position);
+  g_assert (object != NULL);
+  return GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark));
+}
+
+static char *
+model_to_string (GListModel *model)
+{
+  GString *string = g_string_new (NULL);
+  guint i;
+
+  for (i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      if (i > 0)
+        g_string_append (string, " ");
+      g_string_append_printf (string, "%u", get (model, i));
+    }
+
+  return g_string_free (string, FALSE);
+}
+
+static void
+add (GListStore *store,
+     guint       number)
+{
+  GObject *object;
+
+  /* o cannot be differentiated from NULL, so don't use it */
+  g_assert (number != 0);
+
+  object = g_object_new (G_TYPE_OBJECT, NULL);
+  g_object_set_qdata (object, number_quark, GUINT_TO_POINTER (number));
+  g_list_store_append (store, object);
+  g_object_unref (object);
+}
+
+static void
+remove (GListStore *store,
+        guint       position)
+{
+  g_list_store_remove (store, position);
+}
+
+#define assert_model(model, expected) G_STMT_START{ \
+  char *s = model_to_string (G_LIST_MODEL (model)); \
+  if (!g_str_equal (s, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, s, "==", expected); \
+  g_free (s); \
+}G_STMT_END
+
+#define assert_changes(model, expected) G_STMT_START{ \
+  GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \
+  if (!g_str_equal (changes->str, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, changes->str, "==", expected); \
+  g_string_set_size (changes, 0); \
+}G_STMT_END
+
+static GListStore *
+new_empty_store (void)
+{
+  return g_list_store_new (G_TYPE_OBJECT);
+}
+
+static GListStore *
+new_store (guint start,
+           guint end)
+{
+  GListStore *store = new_empty_store ();
+  guint i;
+
+  for (i = start; i <= end; i++)
+    add (store, i);
+
+  return store;
+}
+
+static void
+items_changed (GListModel *model,
+               guint       position,
+               guint       removed,
+               guint       added,
+               GString    *changes)
+{
+  g_assert (removed != 0 || added != 0);
+
+  if (changes->len)
+    g_string_append (changes, ", ");
+
+  if (removed == 1 && added == 0)
+    {
+      g_string_append_printf (changes, "-%u", position);
+    }
+  else if (removed == 0 && added == 1)
+    {
+      g_string_append_printf (changes, "+%u", position);
+    }
+  else
+    {
+      g_string_append_printf (changes, "%u", position);
+      if (removed > 0)
+        g_string_append_printf (changes, "-%u", removed);
+      if (added > 0)
+        g_string_append_printf (changes, "+%u", added);
+    }
+}
+
+static void
+free_changes (gpointer data)
+{
+  GString *changes = data;
+
+  /* all changes must have been checked via assert_changes() before */
+  g_assert_cmpstr (changes->str, ==, "");
+
+  g_string_free (changes, TRUE);
+}
+
+static GtkConcatModel *
+new_model (void)
+{
+  GtkConcatModel *model = gtk_concat_model_new (G_TYPE_OBJECT);
+  GString *changes;
+
+  changes = g_string_new ("");
+  g_object_set_qdata_full (G_OBJECT(model), changes_quark, changes, free_changes);
+  g_signal_connect (model, "items-changed", G_CALLBACK (items_changed), changes);
+
+  return model;
+}
+
+static void
+test_append (void)
+{
+  GListStore *store = new_store (1, 3);
+  GtkConcatModel *concat = new_model ();
+
+  gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+  assert_model (concat, "1 2 3");
+  assert_changes (concat, "0+3");
+
+  g_object_unref (store);
+  g_object_unref (concat);
+}
+
+static void
+test_append_and_add (void)
+{
+  GListStore *store = new_empty_store ();
+  GtkConcatModel *concat = new_model ();
+
+  gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+  add (store, 1);
+  add (store, 2);
+  add (store, 3);
+  assert_model (concat, "1 2 3");
+  assert_changes (concat, "+0, +1, +2");
+
+  g_object_unref (store);
+  g_object_unref (concat);
+}
+
+static void
+test_append_and_remove (void)
+{
+  GListStore *store = new_store (1, 3);
+  GtkConcatModel *concat = new_model ();
+
+  gtk_concat_model_append (concat, G_LIST_MODEL (store));
+  gtk_concat_model_remove (concat, G_LIST_MODEL (store));
+
+  assert_model (concat, "");
+  assert_changes (concat, "0+3, 0-3");
+
+  /* Check that all signal handlers are gone */
+  g_list_store_remove_all (store);
+
+  g_object_unref (store);
+  g_object_unref (concat);
+}
+
+static void
+test_append_and_remove_items (void)
+{
+  GListStore *store = new_empty_store ();
+  GtkConcatModel *concat = new_model ();
+
+  gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+  add (store, 1);
+  add (store, 2);
+  add (store, 3);
+  remove (store, 0);
+  remove (store, 1);
+  remove (store, 0);
+
+  assert_model (concat, "");
+  assert_changes (concat, "+0, +1, +2, -0, -1, -0");
+
+  g_object_unref (store);
+  g_object_unref (concat);
+}
+
+static void
+test_append_many (void)
+{
+  GListStore *store[5] = { new_store (1, 3), new_store (4, 4), new_store (5, 10), new_empty_store (), new_store (11, 20) };
+  GtkConcatModel *concat = new_model ();
+  guint i;
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+
+  assert_model (concat, "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20");
+  assert_changes (concat, "0+3, +3, 4+6, 10+10");
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    g_object_unref (store[i]);
+  g_object_unref (concat);
+}
+
+static void
+test_append_many_and_add (void)
+{
+  GListStore *store[3];
+  GtkConcatModel *concat = new_model ();
+  guint i, j;
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    {
+      store[i] = new_empty_store ();
+      gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    {
+      for (j = 0; j < G_N_ELEMENTS (store); j++)
+        {
+          add (store[(i + j) % G_N_ELEMENTS (store)], i * 3 + j + 1);
+        }
+    }
+
+  assert_model (concat, "1 6 8 2 4 9 3 5 7");
+  assert_changes (concat, "+0, +1, +2, +2, +4, +1, +6, +2, +5");
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    g_object_unref (store[i]);
+  g_object_unref (concat);
+}
+
+static void
+test_append_many_and_remove (void)
+{
+  GListStore *store[5];
+  GtkConcatModel *concat = new_model ();
+  guint i;
+
+  store[0] = new_empty_store ();
+  gtk_concat_model_append (concat, G_LIST_MODEL (store[0]));
+  for (i = 1; i < G_N_ELEMENTS (store); i++)
+    {
+      store[i] = new_store (i * (i - 1) / 2 + 1, i * (i + 1) / 2);
+      gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+    }
+
+  assert_model (concat, "1 2 3 4 5 6 7 8 9 10");
+  assert_changes (concat, "+0, 1+2, 3+3, 6+4");
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    {
+      gtk_concat_model_remove (concat, G_LIST_MODEL (store[(3 * i) % G_N_ELEMENTS (store)]));
+    }
+
+  assert_model (concat, "");
+  assert_changes (concat, "3-3, -0, 2-4, 0-2");
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    {
+      g_list_store_remove_all (store[i]);
+      g_object_unref (store[i]);
+    }
+
+  g_object_unref (concat);
+}
+
+static void
+test_append_many_and_remove_items (void)
+{
+  GListStore *store[5];
+  GtkConcatModel *concat = new_model ();
+  guint i;
+
+  store[0] = new_empty_store ();
+  gtk_concat_model_append (concat, G_LIST_MODEL (store[0]));
+  for (i = 1; i < G_N_ELEMENTS (store); i++)
+    {
+      store[i] = new_store (i * (i - 1) / 2 + 1, i * (i + 1) / 2);
+      gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+    }
+
+  assert_model (concat, "1 2 3 4 5 6 7 8 9 10");
+  assert_changes (concat, "+0, 1+2, 3+3, 6+4");
+
+  for (i = 1; i < G_N_ELEMENTS (store); i++)
+    {
+      remove (store[i], 3 % i);
+    }
+
+  assert_model (concat, "2 5 6 7 8 9");
+  assert_changes (concat, "-0, -1, -1, -6");
+
+  for (i = 0; i < G_N_ELEMENTS (store); i++)
+    g_object_unref (store[i]);
+  g_object_unref (concat);
+}
+
+int
+main (int argc, char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  setlocale (LC_ALL, "C");
+  g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=%s");
+
+  number_quark = g_quark_from_static_string ("Hell and fire was spawned to be released.");
+  changes_quark = g_quark_from_static_string ("What did I see? Can I believe what I saw?");
+
+  g_test_add_func ("/compatmodel/append", test_append);
+  g_test_add_func ("/compatmodel/append_and_add", test_append_and_add);
+  g_test_add_func ("/compatmodel/append_and_remove", test_append_and_remove);
+  g_test_add_func ("/compatmodel/append_and_remove_items", test_append_and_remove_items);
+  g_test_add_func ("/compatmodel/append_many", test_append_many);
+  g_test_add_func ("/compatmodel/append_many_and_add", test_append_many_and_add);
+  g_test_add_func ("/compatmodel/append_many_and_remove", test_append_many_and_remove);
+  g_test_add_func ("/compatmodel/append_many_and_remove_items", test_append_many_and_remove_items);
+
+  return g_test_run ();
+}
index 983a33ffad10833ca12f18f4fe5617a6f1004ba6..fb084ad15caaa19942d4c9706a199089b4877644 100644 (file)
@@ -17,6 +17,7 @@ tests = [
   ['builderparser'],
   ['cellarea'],
   ['check-icon-names'],
+  ['concatmodel', ['../../gtk/gtkconcatmodel.c'], ['-DGTK_COMPILATION', '-UG_ENABLE_DEBUG']],
   ['constraint-solver', [
       '../../gtk/gtkconstraintsolver.c',
       '../../gtk/gtkconstraintexpression.c',